﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using TFDP.Common;
using Microsoft.VisualStudio.Data.Schema.Package.Project;
using Microsoft.Data.Schema.SchemaModel;
using System.Reflection;
using System.IO;
using System.Diagnostics;
using Microsoft.VisualStudio.Shell.Interop;
using System.ComponentModel.Composition;
using System.ComponentModel.Composition.Hosting;
using TFDP.Common.Configuration;
using TFDP.VsPackage.Services;
using System.Windows.Threading;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.Data.Schema.Package.UI;
using VSLangProj;
using VSLangProj80;
using TFDP.Common.T4;
using System.ComponentModel.Design;

namespace TFDP.VsPackage.Support
{
    public class ProjectManager : IDisposable, IDatabaseProjectIdleProcessor
    {
        #region Members
        private bool isInitialized;
        private IDatabaseProjectNode project;

        private Action<string> raiseClearGenerationErrorsAction;
        private Action generationCompleteAction;
        private Action doWorkAction;

        private Dispatcher uiDispatcher;

        private GenericTemplateProcessor genericProcessor;
        #endregion

        #region Constructor
        private ProjectManager(IDatabaseProjectNode project)
        {
            this.project = project;

            // Check if code generation is enabled
            string enableGeneration = project.GetProjectProperty("TFDP_EnableGeneration", false, DatabaseProjectPropertyValueType.EvaluatedValue);
            if (String.IsNullOrEmpty(enableGeneration))
                return;
            if (enableGeneration == Boolean.FalseString)
                return;

            if (this.Compose())
            {
                this.project.ProjectLoaded += new EventHandler<ProjectLoadedEventArgs>(project_ProjectLoaded);
                this.project.AddService(new TemplateManagerService(null));

                this.InitializeProcessor(this.Processor, project, ((IServiceProvider)project));

                this.InitializeWorker();

                this.project.RegisterForIdleProcessing(this);

                this.isInitialized = true;
            }
        }

        private bool Compose()
        {
            string processorName = project.GetProjectProperty("TFDP_ProcessorName", false, DatabaseProjectPropertyValueType.UnevaluatedValue);
            if (String.IsNullOrEmpty(processorName))
            {
                Logger.LogError("No processor configured");
                return false;
            }

            try
            {
                Assembly asm = Assembly.Load(processorName);

                var catalog = new AssemblyCatalog(asm);
                var container = new CompositionContainer(catalog);

                container.ComposeParts(this);
            }
            catch (Exception ex)
            {
                Logger.LogError(String.Format("Processor composition failed: {0}", ex.Message));

                return false;
            }

            return true;
        }

        private void InitializeProcessor(
            IProcessor processor,
            IDatabaseProjectNode project,
            IServiceProvider serviceProvider)
        {
            if (processor == null)
                throw new ArgumentNullException("processor");
            if (project == null)
                throw new ArgumentNullException("project");
            if (serviceProvider == null)
                throw new ArgumentNullException("serviceProvider");

            processor.Initialize(project, serviceProvider);

            processor.GenerationCompleted += new EventHandler<EventArgs>(processor_GenerationCompleted);
            processor.BeginProcessing += new EventHandler<EventArgs>(processor_BeginProcessing);
            processor.QueueChanged += new EventHandler<EventArgs>(processor_QueueChanged);
            processor.TemplateRecompile += new EventHandler<Common.T4.TemplateRecompileEventArgs>(Processor_TemplateRecompile);
        }

        private void InitializeWorker()
        {
            doWorkAction = () =>
            {
                try
                {
                    Processor.DoWork();
                }
                catch (Exception ex)
                {
                    Logger.LogError(ex.Message);
                }
            };

            generationCompleteAction = () => { ProcessGenerationComplete(); };
            raiseClearGenerationErrorsAction = (file) => { RaiseClearGenerationErrors(file); };

            // HACK: Force-create a Dispatcher to use in UI thread operations
            // and such
            uiDispatcher = Dispatcher.CurrentDispatcher;
        }
        #endregion

        #region Properties
        public IExtenderType PropertyExtenderType
        {
            [DebuggerStepThrough]
            get
            {
                if (this.IsInitialized)
                    return this.Processor.PropertyExtenderType;
                else
                    return null;
            }
        }

        public string ProjectDirectory
        {
            get
            {
                return this.project.DirectoryName;
            }
        }

        public bool IsInitialized
        {
            get { return this.isInitialized; }
        }

        internal IVsHierarchy Project
        {
            get
            {
                return this.project.GetIVsHierarchy();
            }
        }

        [Import(typeof(IProcessor))]
        internal IProcessor Processor { get; set; }
        #endregion

        #region Project Model Event Handlers
        private void project_ProjectLoaded(object sender, ProjectLoadedEventArgs e)
        {
            this.project.DataSchemaModel.ModelChanged += new EventHandler<ModelEventArgs>(DataSchemaModel_ModelChanged);
        }

        private void DataSchemaModel_ModelChanged(object sender, Microsoft.Data.Schema.SchemaModel.ModelEventArgs e)
        {
            // hold the validation trigger when the model is currently building
            if (this.project.ModelBuildInProgress)
                return;

            // when there are more than 100 processing operations pending
            // we can hold off validation
            if (this.project.DataSchemaModel.ElementProcessingOperationsCount > 100)
                return;

            DataSchemaModelEventArgs args = e as DataSchemaModelEventArgs;

            // Look for elements in the list of changes
            // that comply to the following rules:
            //
            // - ValidationStatus must be Valid
            // - ModelEventType must be ElementUpdated
            // - The type of element must match any in the list of supported types
            //
            // Changes on an element result in a validation. Non-valid elements must
            // not be processed!
            var elements = from t in args.Changes
                           where t.Item1.ValidationStatus == ModelElementValidationStatus.Valid &&
                                  t.Item2 == Microsoft.Data.Schema.SchemaModel.ModelEventType.ElementUpdated &&
                                  t.Item1.IsMatchingType(Processor.SupportedTypes)
                           select t.Item1;

            if (elements.Count() <= 0)
                return;

            foreach (IModelElement element in elements)
            {
                IDatabaseFileNode node = element.Node(project);

                if (node == null)
                    continue;

                if (node.ItemProperty(CommonProperties.IsGeneratedProperty))
                    continue;

                Enqueue(element);
            }
        }
        #endregion

        #region Processor Events
        private void processor_GenerationCompleted(object sender, EventArgs e)
        {
            if (uiDispatcher.CheckAccess())
                generationCompleteAction();
            else
                uiDispatcher.Invoke(generationCompleteAction);
        }

        public event EventHandler<QueueChangedEventArgs> QueueChanged;

        private void processor_QueueChanged(object sender, EventArgs e)
        {
            if (QueueChanged != null)
                QueueChanged(this, new QueueChangedEventArgs(sender as IProcessor));
        }

        private void processor_BeginProcessing(object sender, EventArgs e)
        {
            SafeRaiseClearGenerationErrors(String.Empty);
        }

        private void Processor_TemplateRecompile(object sender, TemplateRecompileEventArgs e)
        {
            SafeRaiseClearGenerationErrors(e.TemplateFile);
        }

        public event EventHandler<ClearGenerationErrorsEventArgs> ClearGenerationErrors;

        private void SafeRaiseClearGenerationErrors(string file)
        {
            if (uiDispatcher.CheckAccess())
                raiseClearGenerationErrorsAction(file);
            else
                uiDispatcher.Invoke(raiseClearGenerationErrorsAction, new object[] { file });
        }

        private void RaiseClearGenerationErrors(string file)
        {
            if (ClearGenerationErrors != null)
            {
                ClearGenerationErrors(this, new ClearGenerationErrorsEventArgs() { File = file });
            }
        }

        private FileEditRequestResult OnRequestFileEdit(IVsHierarchy projectHierarchy, string path)
        {
            if (RequestFileEdit != null)
            {
                RequestFileEditEventArgs args = new RequestFileEditEventArgs(projectHierarchy, path);
                RequestFileEdit(this, args);

                return args.Result;
            }

            return FileEditRequestResult.Empty;
        }

        public event EventHandler<RequestFileEditEventArgs> RequestFileEdit;
        #endregion

        #region Support
        private SharedStatusBarOwner statusBarOwner;
        private SharedStatusBar statusBar;

        private void ProcessGenerationComplete()
        {
            if (statusBarOwner == null)
                statusBarOwner = new SharedStatusBarOwner();

            if (statusBar == null)
                statusBar = SharedStatusBar.Instance;
            else
                return;

            statusBar.TakeOwnership(statusBarOwner);

            while (true)
            {
                GeneratedArtifact item = Processor.PopGenerationItem();

                if (item == null)
                    break;

                statusBar.SetText(statusBarOwner, "Updating " + item.TargetFileName + "...");

                try
                {
                    // Process GenerationItem
                    WriteOutput(item);
                }
                catch (Exception ex)
                {
                    Logger.LogError(String.Format("Error writing output: {0}", ex.Message));
                }

                // Remove the temporary file
                File.Delete(item.TemporaryFileName);
            }

            statusBar.Clear(statusBarOwner);

            // Release statusbar
            statusBar.ReleaseOwnership(statusBarOwner);

            statusBar = null;
        }

        private void WriteOutput(GeneratedArtifact artifact)
        {
            IFileChangeService fcs = (IFileChangeService)this.project.GetService(typeof(IFileChangeService));

            ITargetProject project = artifact.GetTargetProject();

            if (project is TFDP.VsPackage.Support.TargetProjects.DatabaseProject)
            {
                ((TFDP.VsPackage.Support.TargetProjects.DatabaseProject)project).FileChangeService = this.FileChangeService;
            }

            if (project != null)
            {
                project.Update(artifact);
            }
        }

        private void project_BeforeFileChange(object sender, FileChangeEventArgs e)
        {

        }

        private void project_AfterFileChange(object sender, FileChangeEventArgs e)
        {

        }

        /// <summary>
        /// Obtain the IDatabaseFileNode for the given path
        /// </summary>
        /// <param name="path">The path to locate</param>
        /// <returns>a IDatabaseFileNode if found, otherwise null</returns>
        private IDatabaseFileNode NodeFromPath(string path)
        {
            if (String.IsNullOrEmpty(path))
                throw new ArgumentNullException("path");

            try
            {
                return (IDatabaseFileNode)project.FindNode(path);
            }
            catch (Exception)
            {
                return null;
            }
        }

        private IDatabaseFileNode NodeFromModelElement(IModelElement element)
        {
            if (element == null)
                throw new ArgumentNullException("element");

            if (!(element is IScriptSourcedModelElement))
                throw new InvalidOperationException("element is not ScriptSourced and cannot be mapped to a path");

            return NodeFromPath(((IScriptSourcedModelElement)element).PrimarySource.SourceName);
        }

        private FileEditRequestResult RequestWriteAccess(GeneratedArtifact item)
        {
            if (String.IsNullOrEmpty(item.TargetFileName))
                throw new ArgumentNullException("path");

            // Find the IVsHierarchy for the specified project name
            // Request write access
            if (item.ProjectHierarchy != null)
            {
                return OnRequestFileEdit((IVsHierarchy)item.ProjectHierarchy, item.TargetFileName);
            }
            else
            {
                return new FileEditRequestResult(false, "Unable to obtain target project hierarchy");
            }
        }
        #endregion

        #region Factory
        public static ProjectManager FromProject(IDatabaseProjectNode project)
        {
            return new ProjectManager(project);
        }
        #endregion

        #region IDisposable Members
        private bool isDisposed;
        private object disposeLock = new object();

        public bool IsDisposed
        {
            get
            {
                bool retval = false;

                lock (disposeLock)
                {
                    retval = isDisposed;
                }

                return retval;
            }
        }

        public void Dispose()
        {
            Dispose(true);
        }

        private void Dispose(bool disposing)
        {
            if (!isDisposed)
            {
                lock (disposeLock)
                {
                    if (!isDisposed)
                        isDisposed = true;
                    else
                        return;
                }
            }
            else
                return;

            if (disposing)
            {
                project = null;
            }
        }
        #endregion

        #region Public Interaction
        public void Enqueue(IModelElement element, bool isFromDependencyEnqueue = false)
        {
            if (IsDisposed)
                throw new ObjectDisposedException(this.GetType().Name);

            Processor.Enqueue(element);

            if (!isFromDependencyEnqueue)
            {
                EnqueueDependencies(element);
            }
        }

        public void Enqueue(IDatabaseNode node)
        {
            if (IsDisposed)
                throw new ObjectDisposedException(this.GetType().Name);

            // Check if initialization completed
            if (!this.IsInitialized)
                return;

            string sourceName = node.Url /* .ToUpper() */;

            if (Path.GetExtension(sourceName).ToLower() == ".tt")
            {
                EnqueueGenericTemplate(sourceName);
            }
            else
            {
                EnqueueModelElement(sourceName);
            }
        }

        private void EnqueueGenericTemplate(string sourceName)
        {
            if(genericProcessor == null)
                InitializeGenericProcessor();

            IEnumerable<GeneratedArtifact> artifacts;

            DataSchemaModel model = this.project.DataSchemaModel;

            string buff = genericProcessor.GenerateWithTemplate(sourceName, model, out artifacts);
            GeneratedArtifact artifact = null;

            if (!String.IsNullOrEmpty(buff))
            {
                string targetFileName = Path.Combine(Path.GetDirectoryName(sourceName), Path.GetFileNameWithoutExtension(sourceName) + ".sql");
                string tempFileName = Path.GetTempFileName();
                File.WriteAllText(tempFileName, buff);

                artifact = new GeneratedArtifact(tempFileName, targetFileName, project.Caption, null);
            }

            List<GeneratedArtifact> workset = new List<GeneratedArtifact>(artifacts);

            if(artifact != null)
                workset.Insert(0, artifact);

            workset.ForEach(a =>
            {
                try
                {
                    a.Properties.Add(CommonProperties.AddToParent, Boolean.TrueString);
                    a.Properties.Add(CommonProperties.Parent, sourceName);
                    a.Properties.Add(CommonProperties.OverwriteAlways, Boolean.TrueString);
                    WriteOutput(a);
                }
                catch (Exception)
                {
                }
            });
        }

        private void InitializeGenericProcessor()
        {
            genericProcessor = new GenericTemplateProcessor();
            genericProcessor.ProjectDirectory = project.ProjectFolder;
            genericProcessor.TemplateDirectory = project.FindTemplateDirectory();
            genericProcessor.ProjectId = project.Caption;
            genericProcessor.CurrentConfiguration = project.ConfigurationProvider.ProjectManager.CurrentConfigName;
            genericProcessor.TemplateRecompile += new EventHandler<TemplateRecompileEventArgs>(Processor_TemplateRecompile);
        }

        private void EnqueueModelElement(string sourceName)
        {
            IList<IModelElement> elements = this.project.DataSchemaModel.GetElementsFrom<IModelElement>(sourceName.ToUpper(), ModelElementQueryFilter.Internal);

            var matches = elements
                .Where(e => e is Microsoft.Data.Schema.Sql.SchemaModel.ISqlModelElement)
                .ToList();

            if (matches.Count() > 0)
            {
                matches.ForEach(m => Enqueue(m, true));
            }
        }

        private void EnqueueDependencies(IModelElement modelElement)
        {
            if (modelElement == null)
            {
                throw new ArgumentException("Model Element cannot be null", "modelElement");
            }

            var dependantObjects = modelElement
                .GetReferencingElements()
                .Where(r => r is Microsoft.Data.Schema.Sql.SchemaModel.ISqlForeignKeyConstraint)
                .OfType<Microsoft.Data.Schema.Sql.SchemaModel.ISqlForeignKeyConstraint>()
                .Select(fk => fk.DefiningTable)
                .ToList();

            foreach (var d in dependantObjects)
            {
                this.Processor.Enqueue(d);
            }
        }
        #endregion

        #region IDatabaseProjectIdleProcessor Members
        public void DoWork(DatabaseProjectProjectSyncOption syncOption)
        {
            if (Processor.IsRunning && !Processor.IsBusy && Processor.Count > 0)
            {
                Task t = Task.Factory.StartNew(doWorkAction);
            }
        }

        public void OnIdle()
        {
            DoWork(DatabaseProjectProjectSyncOption.Partial);
        }
        #endregion

        public static bool FilesAreEqual(string sourceFile, string targetFile)
        {
            if (File.Exists(targetFile))
            {

                byte[] sourceBlock = new byte[4096];
                byte[] targetBlock = new byte[4096];

                using (FileStream sourceStream = File.OpenRead(sourceFile))
                using (FileStream targetStream = File.OpenRead(targetFile))
                {
                    long length = 0;
                    int toRead = 0;

                    length = sourceStream.Length;

                    while (length > 0)
                    {
                        toRead = sourceBlock.Length;
                        if (toRead > length)
                            toRead = (int)length;

                        length -= toRead;

                        sourceStream.Read(sourceBlock, 0, toRead);
                        targetStream.Read(targetBlock, 0, toRead);

                        for (int i = 0; i < toRead; i++)
                            if (sourceBlock[i] != targetBlock[i])
                                return false;
                    }
                }
                return true;
            }
            else
                return false;
        }

        public IFileChangeService FileChangeService { get; set; }
    }
}
